package webhook

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	admv "k8s.io/api/admission/v1"
	v1 "k8s.io/api/core/v1"
	v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	"k8s.io/client-go/kubernetes/scheme"
	"net/http"
	"operator/pkg/jsonpatch"
	"operator/pkg/plugins"
	"operator/pkg/types/mesh"
	"operator/pkg/util"
	"os"
	"runtime"
	"strconv"
)

var Scheme = scheme.Scheme
var Codecs = serializer.NewCodecFactory(Scheme)

type KubernetesWebhook struct {
	plugins  []plugins.PluginInterface
	CertFile string
	KeyFile  string
}

func (kw *KubernetesWebhook) Run() {
	mux := http.NewServeMux()
	mux.HandleFunc("/mesh-mutating", kw.Inject)
	err := http.ListenAndServeTLS(":30808", kw.CertFile, kw.KeyFile, mux)

	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func (kw *KubernetesWebhook) Inject(w http.ResponseWriter, r *http.Request) {
	var body []byte
	if r.Body != nil {
		if data, err := ioutil.ReadAll(r.Body); err == nil {
			body = data
		}
	}

	contentType := r.Header.Get("Content-Type")
	if contentType != "application/json" {
		return
	}

	req := admv.AdmissionReview{}
	deserializer := Codecs.UniversalDeserializer()
	if _, _, err := deserializer.Decode(body, nil, &req); err != nil {
		req.Response = toError(err)
	} else {
		res := kw.admit(&req)
		req.Response = res
	}

	req.Response.UID = req.Request.UID
	resByte, _ := json.Marshal(req)

	w.Write(resByte)
}

func (kw *KubernetesWebhook) runPlugin(obj *mesh.Mesh) (*mesh.Mesh, error) {
	new := obj.DeepCopy()

	for _, i := range kw.plugins {
		err := i.Add(new.Spec.Template, obj)
		if err != nil {
			return nil, err
		}
	}

	return new, nil
}

func (kw *KubernetesWebhook) admit(data *admv.AdmissionReview) *admv.AdmissionResponse {
	response := &admv.AdmissionResponse{}
	//deserializer := Codecs.UniversalDeserializer()
	raw := data.Request.Object.Raw

	m := new(mesh.Mesh)
	err := json.Unmarshal(raw, m)
	if err != nil {
		return nil
	}

	response.Allowed = true
	response.UID = data.Request.UID

	if *m.Spec.Count > 0 {
		plugin, err := kw.runPlugin(m)
		if err != nil {
			return nil
		}
		ops := jsonpatch.JsonPatch(m, plugin)
		response.Patch = ops.ToByte()
		pt := admv.PatchTypeJSONPatch
		response.PatchType = &pt
	}

	return response
}

func useCni(obj *mesh.Mesh) bool {
	if data, ok := obj.Annotations[util.UsedCniOrInitContainer]; ok && data == "cni" {
		return true
	}

	return false
}

func NewDefaultWebHook(sip, tip string) *KubernetesWebhook {
	plgs := make([]plugins.PluginInterface, 0, 10)

	privileged := true
	container := func(spec *v1.PodTemplateSpec, objs *mesh.Mesh) error {
		hasContainer := false
		hasInit := false
		for _, i := range spec.Spec.Containers {
			if i.Image == "yh960124/meshproxy" {
				hasContainer = true
				break
			}
		}
		if !useCni(objs) {
			for _, i := range spec.Spec.InitContainers {
				if i.Image == "yh960124/meshinit" {
					hasInit = true
					break
				}
			}
		}

		if !hasContainer {
			sidecar := v1.Container{
				Image:           "yh960124/meshproxy",
				Name:            "meshproxy",
				ImagePullPolicy: "Always",
				SecurityContext: &v1.SecurityContext{
					RunAsUser:  &util.DefaultSidecarUid,
					Privileged: &privileged,
				},
				Env: []v1.EnvVar{
					{
						Name: "POD_NAME",
						ValueFrom: &v1.EnvVarSource{
							FieldRef: &v1.ObjectFieldSelector{
								FieldPath: "metadata.name",
							},
						},
					},
				},
				Args: []string{"-sip", sip, "-port", strconv.Itoa(int(spec.Spec.Containers[0].Ports[0].ContainerPort)), "-version", *objs.Spec.Version, "-tip", tip},
			}
			if len(spec.Spec.Containers) > 0 {
				spec.Spec.Containers = append(spec.Spec.Containers, sidecar)
			} else {
				spec.Spec.Containers = []v1.Container{sidecar}
			}
		}
		if !hasInit {
			init := v1.Container{
				Image:           "yh960124/meshinit",
				Name:            "meshinit",
				ImagePullPolicy: "Always",
				SecurityContext: &v1.SecurityContext{
					Privileged: &privileged,
				},
			}
			if len(spec.Spec.InitContainers) > 0 {
				spec.Spec.InitContainers = append(spec.Spec.InitContainers, init)
			} else {
				spec.Spec.InitContainers = []v1.Container{init}
			}
		}

		return nil
	}
	shared := func(spec *v1.PodTemplateSpec, objs *mesh.Mesh) error {
		shared := true
		spec.Spec.ShareProcessNamespace = &shared
		return nil
	}
	//metrics := func(spec *v1.PodTemplateSpec) error {
	//	m := map[string]string{
	//		"prometheus.io/scrape": "true",
	//		"prometheus.io/path":   "/metrics",
	//		"prometheus.io/port":   strconv.Itoa(util.SidecarMetricsPort),
	//	}
	//	if len(spec.Annotations) > 0 {
	//		for k, v := range m {
	//			spec.Annotations[k] = v
	//		}
	//	} else {
	//		spec.Annotations = m
	//	}
	//
	//	return nil
	//}

	plgs = append(plgs, plugins.NewPodPlugin(container, shared))
	//plgs = append(plgs, plugins.NewPodPlugin(container, shared, metrics))

	if runtime.GOOS == "windows" {
		k := &KubernetesWebhook{
			plugins:  plgs,
			KeyFile:  "D:\\gopath\\src\\operator\\config\\ca/server.key",
			CertFile: "D:\\gopath\\src\\operator\\config\\ca/server.crt",
		}

		return k

	}
	k := &KubernetesWebhook{
		plugins:  plgs,
		KeyFile:  "/etc/webhook/certs/server.key",
		CertFile: "/etc/webhook/certs/server.crt",
	}

	return k
}

func NewWebHook(sip string, opts ...plugins.PluginInterface) *KubernetesWebhook {
	k := &KubernetesWebhook{
		plugins: opts,
	}
	return k
}

func toError(err error) *admv.AdmissionResponse {
	return &admv.AdmissionResponse{
		Result: &v12.Status{
			Message: err.Error(),
		},
	}
}
